Skip to content

refactor, feat: 네트워크 리팩토링, 점검 페이지 구현#362

Merged
hgjwilly-koreatech merged 18 commits intodevelopfrom
refactor/network
Feb 15, 2026
Merged

refactor, feat: 네트워크 리팩토링, 점검 페이지 구현#362
hgjwilly-koreatech merged 18 commits intodevelopfrom
refactor/network

Conversation

@hgjwilly-koreatech
Copy link
Contributor

@hgjwilly-koreatech hgjwilly-koreatech commented Feb 13, 2026

#️⃣연관된 이슈

📝작업 내용

이번 PR에서 작업한 내용을 간략히 설명해주세요(이미지 첨부 가능)

  • 각 도메인 Service의 request 메서드를 삭제하고, NetworkService의 request 메서드를 사용하도록 변경
    NetworkService에 반환타입이 request 메서드가 구현되어있지만, 일부 도메인 Service에서 request 메서드를 별도 구현하고 혼용해서 사용하고 있습니다.
    각 도메인 Service에 별도 구현된 request 메서드를 제거하고, NetworkService로 통합합니다.

  • 반환 타입이 <T, Error>, <Void, Error> 인 request 메서드를 삭제하고 <T, ErrorResponse>, <Void, ErrorResponse> 로 통합
    ErrorResponse가 정의되어있음에도 Error를 혼용해서 사용하고있습니다.
    ErrorResponse만을 사용하도록 변경합니다.

  • ErrorResponse 를 직접 생성하지 않고, 미리 정해진 static 값을 사용하도록 변경
    에러 발생시 ErrorResponse(code:message:)를 직접 생성해서 사용하고 있습니다.
    ErrorResponse에 static으로 선언되어있는 값을 사용하도록 변경했습니다.

  • Interceptor의 retry 메서드로 토큰 갱신 구현
    각 도메인 Service의 메서드에서 토큰 갱신을 직접 구현하고 있으며, 토큰 갱신이 누락된 메서드도 다수 존재합니다.
    NetworkService의 request 메서드에서 Interceptor를 이용하여 토큰 갱신하도록 변경했습니다.

  • Interceptor의 adapt 메서드로 access token을 헤더에 추가 구현
    API에서 헤더에 access token을 직접 추가하지 않고,
    NetworkService의 request 메서드에서 Interceptor를 이용하여 access token을 헤더에 추가하도록 변경했습니다.

  • 점검 페이지 UI 구현, 5XX 오류시 점검 페이지로 이동
    5XX 오류 점검 페이지를 present 합니다.
    '메인 화면 바로가기' 버튼을 누르면 HomeViewController로 돌아갑니다.

  • KeychainWorker 성능 개선
    모든 API 호출에서 access token을 헤더에 주입하기 위해 Keychain에 접근합니다.
    성능 개선을 위해 KeychainWorker에 딕셔너리를 추가하고 값을 캐싱합니다.

스크린샷 (선택)

ezgif-6c5e097f1ba2b267

💬리뷰 요구사항(선택)

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the iOS networking layer by consolidating per-domain request/refresh logic into a single NetworkService backed by an Alamofire RequestInterceptor (adapt + retry), and adds a global “maintenance/error” screen that is presented on server 5XX responses.

Changes:

  • Centralize requests into NetworkService and remove duplicated per-service token-refresh logic.
  • Add Interceptor to inject access tokens and retry once after refreshing tokens on 401.
  • Introduce ErrorViewController and present it via SceneDelegate when 5XX errors occur.

Reviewed changes

Copilot reviewed 31 out of 33 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
koin.xcodeproj/project.pbxproj Adds new networking/UI/CoreData files to the Xcode project and reorganizes groups.
Koin/Presentation/Home/Home/SubViews/ErrorViewController.swift New error/maintenance UI screen with “go to main” action.
Koin/Data/Service/UserService.swift Removes per-method refresh logic and routes calls through unified NetworkService.
Koin/Data/Service/TimetableService.swift Replaces local Alamofire request helper with NetworkService.
Koin/Data/Service/ShopService.swift Removes retry flag + refresh logic; routes via NetworkService.
Koin/Data/Service/NoticeListService.swift Removes retry flag + refresh logic; routes via NetworkService + CoreData fallback paths.
Koin/Data/Service/NotiService.swift Removes refresh logic and relies on interceptor-based auth/retry.
Koin/Data/Service/Network/NetworkService.swift Adds interceptor-backed request overloads and posts “ServerError” notification on 5XX.
Koin/Data/Service/Network/MockNetworkService.swift Adds a mock network service implementation (not yet integrated into services).
Koin/Data/Service/Network/Interceptor.swift Adds Alamofire RequestInterceptor for auth header injection + refresh-on-401 retry.
Koin/Data/Service/Network/API/UserAPI.swift Removes per-endpoint Authorization header injection (delegated to interceptor).
Koin/Data/Service/Network/API/TimetableAPI.swift Removes per-endpoint Authorization header injection (delegated to interceptor).
Koin/Data/Service/Network/API/ShopAPI.swift Removes per-endpoint Authorization header injection (delegated to interceptor).
Koin/Data/Service/Network/API/NoticeListAPI.swift Removes per-endpoint Authorization header injection (delegated to interceptor).
Koin/Data/Service/Network/API/NotiAPI.swift Removes Authorization header injection; keeps Content-Type only.
Koin/Data/Service/Network/API/LostItemAPI.swift Removes per-endpoint Authorization header injection (delegated to interceptor).
Koin/Data/Service/Network/API/DiningAPI.swift Removes per-endpoint Authorization header injection (delegated to interceptor).
Koin/Data/Service/Network/API/ChatAPI.swift Removes per-endpoint Authorization header injection (delegated to interceptor).
Koin/Data/Service/Network/API/AbTestAPI.swift Removes Authorization header injection (delegated to interceptor).
Koin/Data/Service/LostItemService.swift Replaces local request helpers and removes refresh logic.
Koin/Data/Service/LogAnalyticsService.swift Minor formatting-only change.
Koin/Data/Service/LandService.swift Replaces local request helper with NetworkService.
Koin/Data/Service/DiningService.swift Removes retry flag + refresh logic; routes via NetworkService.
Koin/Data/Service/CoreService.swift Replaces local request helper with NetworkService.
Koin/Data/Service/CoreData/CoreDataService.swift Adds a CoreData service wrapper (singleton) and moves it into a new group.
Koin/Data/Service/ChatService.swift Removes refresh logic and routes via unified NetworkService.
Koin/Data/Service/BusService.swift Switches from mock request helper to NetworkService.
Koin/Data/Service/AbTestService.swift Removes retry flag/refresh logic and preserves accessHistoryId bootstrap flow.
Koin/Data/Repository/DefaultShopRepository.swift Updates service call signature (removes retry parameter).
Koin/Data/Repository/DefaultNoticeListRepository.swift Updates service call signature (removes retry parameter).
Koin/Data/Repository/DefaultDiningRepository.swift Updates service call signature (removes retry parameter).
Koin/Data/Repository/DefaultAbTestRepository.swift Updates service call signature (removes retry parameter).
Koin/Apps/SceneDelegate.swift Observes “ServerError” notification and presents ErrorViewController.
Comments suppressed due to low confidence (2)

Koin/Data/Service/Network/NetworkService.swift:121

  • uploadFiles uses api.asMultipartRequest(...) which uploads without the NetworkService interceptor. Since auth headers were removed from ShopAPI, this upload will no longer receive the Authorization header or 401 refresh retry. Route uploads through an Alamofire Session/request that uses the same interceptor, or extend asMultipartRequest/NetworkService to accept and apply the interceptor.
    Koin/Data/Service/Network/NetworkService.swift:170
  • Server-error detection relies on ErrorResponse.code being a numeric string. When decoding fails (e.g. non-JSON 5XX body) you create ErrorResponse(code: "", ...), so the maintenance screen won’t be triggered. Consider using the HTTP status code (available in the response) to trigger the ServerError notification, and/or populating code with the status code when you can’t decode.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 69 to 72
func checkLogin() -> AnyPublisher<Bool, Never> {
networkService.request(api: UserAPI.checkLogin)
return (networkService.request(api: UserAPI.checkLogin) as AnyPublisher<Void, ErrorResponse>)
.map { _ in true }
.catch { [weak self] error -> AnyPublisher<Bool, Never> in
guard let self = self else {
return Just(false).eraseToAnyPublisher()
}
if error.code == "401" {
return self.networkService.refreshToken()
.flatMap { _ in self.networkService.request(api: UserAPI.checkLogin) }
.map { _ in true }
.replaceError(with: false)
.eraseToAnyPublisher()
} else {
return Just(false).eraseToAnyPublisher()
}
}
.replaceError(with: false)
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These explicit casts are needed because NetworkService overloads differ only by failure type. This is brittle and can hide type mismatches at runtime. Consider renaming the overloads (e.g. requestErrorResponse vs request) or using a generic error wrapper so call sites don’t require forced casts.

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +10
import UIKit

final class ErrorViewController: UIViewController {
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then and SnapKit (.snp) are used in this file, but only UIKit is imported. This won’t compile unless you add the needed imports (e.g. Then/SnapKit) or remove the usages.

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +44
private let titleLabel = UILabel().then {
$0.text = "개발 중인 페이지입니다"
$0.font = .appFont(.pretendardSemiBold, size: 20)
$0.textColor = .appColor(.primary500)
}

private let subTitleLabel = UILabel().then {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineHeightMultiple = 1.40
paragraphStyle.alignment = .center

$0.attributedText = NSAttributedString(
string: "죄송합니다. 현재 개발 중인 페이지입니다.\n최대한 빠르게 오픈하도록 하겠습니다.",
attributes: [
.font : UIFont.appFont(.pretendardMedium, size: 14),
.foregroundColor : UIColor.appColor(.neutral500),
.paragraphStyle : paragraphStyle
])
$0.numberOfLines = 2
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The copy says "개발 중인 페이지입니다" which reads like a “work in progress” page, but this view is presented on 5XX server errors per the PR description. Consider updating the title/subtitle to a maintenance/outage message so users aren’t misled.

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +18
import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

var window: UIWindow?
var urlParameters: [String: String]?

override init() {
super.init()
NotificationCenter.default.addObserver(self, selector: #selector(presentErrorViewController), name: NSNotification.Name("ServerError"), object: nil)
}
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then is used later when creating ErrorViewController, but this file doesn’t import Then. This will fail to compile unless Then is imported (or the .then usage is removed).

Copilot uses AI. Check for mistakes.
Comment on lines 123 to 135
@objc private func presentErrorViewController() {

if let navigationController = window?.rootViewController as? UINavigationController {

let homeViewController = makeHomeViewController()
let completion: ()->Void = {
navigationController.setViewControllers([homeViewController], animated: false)
}
let errorViewController = ErrorViewController(completion: completion).then {
$0.modalPresentationStyle = .fullScreen
}
navigationController.present(errorViewController, animated: true)
}
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

presentErrorViewController can be invoked on a background thread (the notification is posted from networking code). Presenting view controllers must happen on the main thread, and repeated 5XXs can trigger multiple presentations. Dispatch to the main queue and guard against presenting the error screen more than once.

Copilot uses AI. Check for mistakes.
Comment on lines 20 to 24
private let waringImageView = UIImageView().then {
let configuration = UIImage.SymbolConfiguration.init(pointSize: 60, weight: .light)
$0.image = UIImage(systemName: "exclamationmark.triangle", withConfiguration: configuration)
$0.tintColor = .appColor(.neutral500)
}
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Property name waringImageView looks like a typo (should be warningImageView). This makes the code harder to read/search and propagates throughout constraints/usages.

Copilot uses AI. Check for mistakes.
Comment on lines +64 to +69
private func requestWithResponse<T: Decodable>(api: URLRequestConvertible) -> AnyPublisher<T, Error> {
return AF.request(api)
.publishDecodable(type: T.self)
.value()
.mapError { $0 as Error }
.eraseToAnyPublisher()
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refreshToken() uses AF.request(api).publishDecodable(...).value() without .validate(), so non-2xx responses may be treated as successes if they decode. Add validation (or explicit status-code checks) so refresh only succeeds on 2xx.

Copilot uses AI. Check for mistakes.
Comment on lines 40 to 43

refreshToken().sink( receiveValue: { isRefreshed in
completion(isRefreshed ? .retry : .doNotRetryWithError(error))
}).store(in: &subscriptions)
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each retry stores a new AnyCancellable into subscriptions and never removes it. Over time this can grow unbounded (even though publishers finish). Consider removing cancellables on completion, or using a different approach that doesn’t retain every finished subscription.

Suggested change
refreshToken().sink( receiveValue: { isRefreshed in
completion(isRefreshed ? .retry : .doNotRetryWithError(error))
}).store(in: &subscriptions)
var cancellable: AnyCancellable?
cancellable = refreshToken()
.sink(
receiveCompletion: { [weak self] _ in
if let cancellable = cancellable {
self?.subscriptions.remove(cancellable)
}
},
receiveValue: { isRefreshed in
completion(isRefreshed ? .retry : .doNotRetryWithError(error))
}
)
if let cancellable = cancellable {
subscriptions.insert(cancellable)
}

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 6
//
// Retrier.swift
// koin
//
// Created by 홍기정 on 2/12/26.
//
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Header comment says Retrier.swift but the file/class is Interceptor. This mismatch makes file navigation/search confusing; please update the header to match the filename.

Copilot uses AI. Check for mistakes.
Comment on lines 77 to 83
func createNotificationKeyword(requestModel: NoticeKeywordDto) -> AnyPublisher<NoticeKeywordDto, ErrorResponse> {
return networkService.requestWithResponse(api: NoticeListAPI.createNotificationKeyword(requestModel))
.catch { [weak self] error -> AnyPublisher<NoticeKeywordDto, ErrorResponse> in
guard let self = self else { return Fail(error: error).eraseToAnyPublisher() }
if error.code == "401" {
return self.networkService.refreshToken()
.flatMap { _ in self.networkService.requestWithResponse(api: NoticeListAPI.createNotificationKeyword(requestModel))
}
.catch { [weak self] _ -> AnyPublisher<NoticeKeywordDto, ErrorResponse> in
guard let self = self else { return Fail(error: error).eraseToAnyPublisher() }
return self.createCoreDataKeyword(requestModel: requestModel)
}
.eraseToAnyPublisher()
} else {
return self.createCoreDataKeyword(requestModel: requestModel)
}
guard let self else { return Fail(error: error).eraseToAnyPublisher() }
return self.createCoreDataKeyword(requestModel: requestModel)
}
.eraseToAnyPublisher()
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createNotificationKeyword now falls back to createCoreDataKeyword for any error, which also inserts into CoreData and returns an error message implying login failure. This changes behavior for non-auth errors (e.g. 400/500/network) and can create unintended local keywords. Consider only falling back for specific cases (e.g. refresh failure / not logged in), and ensure the returned error message matches the actual failure.

Copilot uses AI. Check for mistakes.
여러 API를 동시에 호출할 때 토큰 갱신 로직이 중복 호출될 수 있는 문제를 개선했습니다.
스레드 안정성을 개선했습니다.
API를 호출할 때마다 헤더에 access token을 추가하기 위해 키체인에 접근하고 있습니다.
KeychainWorker에 캐싱기능을 추가했습니다.
이미 present 중인 viewcontroller가 있는 경우, dismiss 후 present 합니다.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 32 out of 34 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (4)

Koin/Data/Service/Network/NetworkService.swift:171

  • handleError(_ error: Error) -> ErrorResponse only handles ErrorResponse and otherwise returns a generic “알 수 없는 에러”. In the ErrorResponse request paths you sometimes throw an AFError (e.g., unacceptableStatusCode when the body is empty), so 5XX errors can fail to post the ServerError notification and you lose the HTTP status code context. Consider handling AFError here (or posting based on httpResponse.statusCode before throwing) so 5XX reliably triggers the maintenance flow and the returned ErrorResponse preserves the status code.
    Koin/Data/Service/Network/NetworkService.swift:101
  • uploadFiles uses api.asMultipartRequest(...) which internally calls AF.upload(...) without the RequestInterceptor. Since this PR removes Authorization injection from API headers and relies on Interceptor.adapt, multipart uploads will no longer include the access token and may start failing with 401. Consider performing the upload through a Session/AF.upload call that includes the same interceptor (or manually add the Authorization header here).
    Koin/Data/Service/Network/NetworkService.swift:29
  • request(api:) is declared twice with the same parameter list and only the return type differs (AnyPublisher<Void, Error> vs AnyPublisher<Void, ErrorResponse>). Swift doesn’t support overloading solely by return type, so this won’t compile. Rename one API (e.g., requestVoid / requestVoidWithErrorResponse) or introduce a generic error wrapper so the signatures differ at the call site.
    Koin/Data/Service/Network/NetworkService.swift:69
  • requestWithResponse(api:) is also declared twice with identical parameter lists and only differing by return error type (Error vs ErrorResponse). This is not a valid overload in Swift and will cause an “invalid redeclaration” compile error. Consider renaming or using a single method that maps to the desired error type via an explicit parameter or separate wrapper.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +141 to +143
let errorViewController = ErrorViewController(completion: completion).then {
$0.modalPresentationStyle = .fullScreen
}
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ErrorViewController(...).then { ... } requires the Then library, but this file only imports UIKit. Unless there’s a module-wide re-export (none found), this will not compile. Add import Then here or avoid .then and set modalPresentationStyle directly.

Copilot uses AI. Check for mistakes.
Comment on lines 128 to 150

guard isPresentingPresentErrorViewController == false else {
return
}
isPresentingPresentErrorViewController = true

if let navigationController = window?.rootViewController as? UINavigationController {

let homeViewController = makeHomeViewController()
let completion: ()->Void = { [weak self] in
self?.isPresentingPresentErrorViewController = false
navigationController.setViewControllers([homeViewController], animated: false)
}
let errorViewController = ErrorViewController(completion: completion).then {
$0.modalPresentationStyle = .fullScreen
}

if let _ = navigationController.presentedViewController {
navigationController.dismiss(animated: true) {
navigationController.present(errorViewController, animated: true)
}
} else {
navigationController.present(errorViewController, animated: true)
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

presentErrorViewController() can be invoked from a background thread because the ServerError notification is posted from networking code. This method performs UIKit operations (present, dismiss, setViewControllers), which must run on the main thread. Wrap the body in DispatchQueue.main.async { ... } (or ensure the notification is delivered on the main queue).

Suggested change
guard isPresentingPresentErrorViewController == false else {
return
}
isPresentingPresentErrorViewController = true
if let navigationController = window?.rootViewController as? UINavigationController {
let homeViewController = makeHomeViewController()
let completion: ()->Void = { [weak self] in
self?.isPresentingPresentErrorViewController = false
navigationController.setViewControllers([homeViewController], animated: false)
}
let errorViewController = ErrorViewController(completion: completion).then {
$0.modalPresentationStyle = .fullScreen
}
if let _ = navigationController.presentedViewController {
navigationController.dismiss(animated: true) {
navigationController.present(errorViewController, animated: true)
}
} else {
navigationController.present(errorViewController, animated: true)
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
guard self.isPresentingPresentErrorViewController == false else {
return
}
self.isPresentingPresentErrorViewController = true
if let navigationController = self.window?.rootViewController as? UINavigationController {
let homeViewController = self.makeHomeViewController()
let completion: ()->Void = { [weak self] in
self?.isPresentingPresentErrorViewController = false
navigationController.setViewControllers([homeViewController], animated: false)
}
let errorViewController = ErrorViewController(completion: completion).then {
$0.modalPresentationStyle = .fullScreen
}
if let _ = navigationController.presentedViewController {
navigationController.dismiss(animated: true) {
navigationController.present(errorViewController, animated: true)
}
} else {
navigationController.present(errorViewController, animated: true)
}

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,117 @@
//
// Retrier.swift
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File header comment says Retrier.swift, but the file/class is Interceptor. This can confuse future maintenance and file searches—please update the header to match the actual filename/type.

Suggested change
// Retrier.swift
// Interceptor.swift

Copilot uses AI. Check for mistakes.
Comment on lines +105 to +107
wrapperView.snp.makeConstraints {
$0.center.equalTo(wrapperViewLayoutGuide)
}
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrapperView is only centered inside wrapperViewLayoutGuide and its subviews are only constrained via centerX/vertical constraints. There are no leading/trailing (or width) constraints tying the content to a deterministic width, which can lead to ambiguous layout warnings and unexpected wrapping on different devices. Consider constraining wrapperView (or at least subTitleLabel) with leading/trailing insets and/or setting a max width relative to the safe area.

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +39
private let titleLabel = UILabel().then {
$0.text = "개발 중인 페이지입니다"
$0.font = .appFont(.pretendardSemiBold, size: 20)
$0.textColor = .appColor(.primary500)
}

private let subTitleLabel = UILabel().then {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineHeightMultiple = 1.40
paragraphStyle.alignment = .center

$0.attributedText = NSAttributedString(
string: "죄송합니다. 현재 개발 중인 페이지입니다.\n최대한 빠르게 오픈하도록 하겠습니다.",
attributes: [
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR/issue description says this screen is shown on 5XX server errors (점검/장애 상황), but the UI copy here says “개발 중인 페이지입니다”. This looks like a mismatch with the intended maintenance page behavior; consider updating the title/subtitle strings to reflect a server 점검/일시적 오류 message.

Copilot uses AI. Check for mistakes.
AF.upload 를 호출하는 Router의 asMultipartRequest 메서드를, asMultipartFormData 로 변경했습니다.
NetworkService에서 직접 AF.upload 를 호출하며 Interceptor를 사용합니다.
tryMap에서 Error, URLError를 throw하면 handleError 에서 Error를 ErrorResponse로 캐스팅에 실패합니다. tryMap에서 ErrorResponse만 throw하도록 개선했습니다.
handleError에서 statusCode를 확인할 수 있도록 하기 위해, ErrorRespose의 프로퍼티에 statusCode를 추가했습니다.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 35 out of 37 changed files in this pull request and generated 8 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +26 to +30
private let titleLabel = UILabel().then {
$0.text = "개발 중인 페이지입니다"
$0.font = .appFont(.pretendardSemiBold, size: 20)
$0.textColor = .appColor(.primary500)
}
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description/issue says this screen is shown for 5XX “점검 페이지”, but the UI copy here says “개발 중인 페이지입니다” and apologizes for a page under development. This is likely the wrong message for a server maintenance/outage state; please update the strings to match the intended 5XX maintenance context.

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +30
func request(api: URLRequestConvertible) -> AnyPublisher<Void, ErrorResponse> {
return AF.request(api, interceptor: interceptor)
.publishData()
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This second request(api:) has the same signature as the earlier one (only the Failure type differs). This is an invalid overload in Swift and will break compilation. Consider exposing a single public request API (e.g., always returning ErrorResponse) and providing a helper to erase/map to Error where needed.

Copilot uses AI. Check for mistakes.
import Foundation

struct ErrorResponse: Decodable, Error {
var statusCode: Int?
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

statusCode was added to ErrorResponse without a default value / custom initializer. This changes the synthesized memberwise init and will break existing call sites like ErrorResponse(code:..., message:...) across the app. Consider giving statusCode a default (var statusCode: Int? = nil) or adding an explicit init with statusCode defaulting to nil to keep source compatibility.

Suggested change
var statusCode: Int?
var statusCode: Int? = nil

Copilot uses AI. Check for mistakes.
Comment on lines 13 to 14
private var isPresentingPresentErrorViewController = false

Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The flag name isPresentingPresentErrorViewController looks like a typo (“PresentingPresent”). Renaming to something like isPresentingErrorViewController would improve readability and reduce confusion when maintaining this logic.

Suggested change
private var isPresentingPresentErrorViewController = false
private var isPresentingErrorViewController = false
@available(*, deprecated, message: "Use isPresentingErrorViewController instead.")
private var isPresentingPresentErrorViewController: Bool {
get { return isPresentingErrorViewController }
set { isPresentingErrorViewController = newValue }
}

Copilot uses AI. Check for mistakes.
Comment on lines 16 to 18
func request(api: URLRequestConvertible) -> AnyPublisher<Void, Error> {
return AF.request(api, interceptor: interceptor)
.validate()
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

request(api:) is declared twice in this type with the same parameter list and only the return type differing (AnyPublisher<Void, Error> vs AnyPublisher<Void, ErrorResponse>). Swift can’t overload functions by return type alone, so this won’t compile. Please rename one of the methods (e.g., requestVoid / requestVoidWithErrorResponse) or collapse to a single error type and map at call sites.

Copilot uses AI. Check for mistakes.
Comment on lines 50 to 52
func requestWithResponse<T: Decodable>(api: URLRequestConvertible) -> AnyPublisher<T, Error> {
return AF.request(api, interceptor: interceptor)
.validate()
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requestWithResponse(api:) is also declared twice with identical parameters and only differing return type (AnyPublisher<T, Error> vs AnyPublisher<T, ErrorResponse>). This kind of return-type-only overloading won’t compile in Swift. Rename the methods or consolidate to a single failure type.

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +63
func requestWithResponse<T: Decodable>(api: URLRequestConvertible) -> AnyPublisher<T, ErrorResponse> {
return AF.request(api, interceptor: interceptor)
.publishData()
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This second generic requestWithResponse(api:) duplicates the earlier definition with the same parameter list, differing only in the failure type. Swift cannot disambiguate by return type, so this must be refactored (e.g., different method names or a single failure type).

Copilot uses AI. Check for mistakes.
Comment on lines 31 to 35
.tryMap { response in
guard let httpResponse = response.response else {
throw ErrorResponse(code: "NETWORK_ERROR", message: "서버 응답 오류")
}
if 200..<300 ~= httpResponse.statusCode {
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file still constructs ErrorResponse(code:message:) in several places (e.g., network/multipart failures). With the newly added statusCode member (and no default/init), these initializers won’t compile. After stabilizing ErrorResponse’s initializer (e.g., default statusCode = nil), please re-check this file for remaining call sites.

Copilot uses AI. Check for mistakes.
기본값 nil을 명시했습니다.
각 sink 로직에 중복된 로깅을 NetworkService로 분리합니다.
tryMap에서 URLError, AFError 를 throw 하면 handleError에서 항상 캐스팅에 실패하므로, ErrorResponse만을 throw 하도록 변경했습니다.
ErrorResponse와 ErrorResponseDto를 분리했습니다.
ErrorResponse를 직접 생성해여 throw 하는 기존 로직을, 미리 정해진 error를 throw 하도록 리팩토링했습니다.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 127 out of 128 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 41 to 44
static let fileManagerFailedDirectory = ErrorResponse(statusCode: nil, code: "FILEMANAGER_FAILED_DIRECTORY", message: "파일 저장 위치 찾기 실패")
static let deleteKeywordError = ErrorResponse(statusCode: nil, code: "DELETE_KEYWORD_ERROR", message: "로그인에 실패하여 코어데이터에서 키워드 삭제")
static let createKeywordError = ErrorResponse(statusCode: nil, code: "CREATE_KEYWORD_ERROR", message: "로그인에 실패하여 코어데이터에서 키워드 삭제")
}
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createKeywordError uses the same message as deleteKeywordError ('키워드 삭제'). This makes the error misleading when keyword creation fails; adjust the message to reflect saving/creating the keyword.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +74
} else if status == errSecItemNotFound {
keychains.updateValue(nil, forKey: key)
return nil
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dictionaries can’t store nil values; keychains.updateValue(nil, forKey:) removes the key. This means the errSecItemNotFound path isn’t actually cached and read will query the Keychain again next time. If you want to cache misses, use a non-optional value type (e.g. enum) or track not-found keys separately.

Copilot uses AI. Check for mistakes.
Comment on lines 284 to 287
receiveValue: { [weak self] _ in
self?.lectureData.removeAll {
$0.classTime == lecture.classTime && $0.name == lecture.name && $0.professor == $0.professor
}
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removeAll predicate compares $0.professor == $0.professor, which is always true. This will delete every lecture matching classTime+name regardless of professor. Compare against lecture.professor instead (same as deleteLectureById).

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +45
private let titleLabel = UILabel().then {
$0.text = "개발 중인 페이지입니다"
$0.font = .appFont(.pretendardSemiBold, size: 20)
$0.textColor = .appColor(.primary500)
}

private let subTitleLabel = UILabel().then {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineHeightMultiple = 1.40
paragraphStyle.alignment = .center

$0.attributedText = NSAttributedString(
string: "죄송합니다. 현재 개발 중인 페이지입니다.\n최대한 빠르게 오픈하도록 하겠습니다.",
attributes: [
.font : UIFont.appFont(.pretendardMedium, size: 14),
.foregroundColor : UIColor.appColor(.neutral500),
.paragraphStyle : paragraphStyle
])
$0.numberOfLines = 2
}
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says this screen is a maintenance page shown on 5XX server errors, but the title/subtitle copy says '개발 중인 페이지입니다'. This looks like the wrong user-facing message for a 5XX maintenance scenario; update the copy to reflect server 점검/장애 안내.

Copilot uses AI. Check for mistakes.
Comment on lines 137 to 141
let completion: ()->Void = { [weak self] in
self?.isPresentingErrorViewController = false
navigationController.setViewControllers([homeViewController], animated: false)
}
let errorViewController = ErrorViewController(completion: completion).then {
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isPresentingErrorViewController is set back to false inside the button completion before the error VC is dismissed. If another 5XX occurs during the dismissal animation, this can re-enter and attempt to present again. Consider resetting the flag in the dismissal completion (or after presentedViewController is actually dismissed) instead of immediately.

Suggested change
let completion: ()->Void = { [weak self] in
self?.isPresentingErrorViewController = false
navigationController.setViewControllers([homeViewController], animated: false)
}
let errorViewController = ErrorViewController(completion: completion).then {
var errorViewController: ErrorViewController!
let completion: ()->Void = { [weak self, weak navigationController, weak errorViewController] in
guard let self = self,
let navigationController = navigationController,
let errorViewController = errorViewController else { return }
errorViewController.dismiss(animated: true) {
self.isPresentingErrorViewController = false
navigationController.setViewControllers([homeViewController], animated: false)
}
}
errorViewController = ErrorViewController(completion: completion).then {

Copilot uses AI. Check for mistakes.
@hgjwilly-koreatech hgjwilly-koreatech merged commit 86ebb84 into develop Feb 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

FEAT 기능 개발 및 api 연동 REFACTOR 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant